package com.tukez.tmdb.util
import java.io.InputStream
import java.io.File
import java.io.FileInputStream
import java.nio.channels.FileChannel.MapMode
import java.nio.ByteOrder
import java.nio.ByteBuffer
import java.nio.LongBuffer
import scala.annotation.tailrec
import com.tukez.tmdb.query.Hash
import java.io.ByteArrayInputStream
import java.io.DataInputStream

object TMDbUtil {

  private val hashChunkSize = 64 * 1024

  /** Return hash for a file in TMDb format. Return None if file is smaller than 64KB
   */
  def hashFor(filename: String): Option[Hash] = hashFor(new File(filename))

  /** Return hash for a file in TMDb format. Return None if file is smaller than 64KB
   */
  def hashFor(file: File): Option[Hash] = {
    val bytesize = file.length
    if (bytesize < hashChunkSize) return None

    val fis = new FileInputStream(file)
    try {
      val channel = fis.getChannel()

      val headBuffer = channel.map(MapMode.READ_ONLY, 0, hashChunkSize)
      val tailBuffer = channel.map(MapMode.READ_ONLY, bytesize - hashChunkSize, hashChunkSize)

      Some(hashFor(bytesize, headBuffer, tailBuffer))
    } finally {
      fis.close()
    }
  }

  /** Return hash for a data in InputStream. Does not close the InputStream
   *  @param bytesize must be greater or equal than 64KB
   */
  def hashFor(is: InputStream, bytesize: Int): Hash = {
    if (bytesize < hashChunkSize) throw new IllegalArgumentException("bytesize must be at least 64KB")

    val buffer = new Array[Byte]((hashChunkSize * 2) min bytesize)
    val bais = new DataInputStream(is)

    bais.readFully(buffer, 0, hashChunkSize)
    bais.skipBytes((bytesize - hashChunkSize * 2) max 0)
    bais.readFully(buffer, hashChunkSize, hashChunkSize min (buffer.length - hashChunkSize))

    val headBuffer = ByteBuffer.wrap(buffer, 0, hashChunkSize)
    val tailBuffer = ByteBuffer.wrap(buffer, buffer.length - hashChunkSize, hashChunkSize)

    hashFor(bytesize, headBuffer, tailBuffer)
  }

  private def hashFor(bytesize: Long, headBuffer: ByteBuffer, tailBuffer: ByteBuffer): Hash = {
    @tailrec
    def sumFor(buffer: LongBuffer, sum: Long = 0): Long = {
      if (!buffer.hasRemaining) sum
      else sumFor(buffer, sum + buffer.get())
    }
    def hashFor(buffer: ByteBuffer): Long = {
      val longBuffer = buffer.order(ByteOrder.LITTLE_ENDIAN).asLongBuffer
      sumFor(longBuffer)
    }

    val headHash = hashFor(headBuffer)
    val tailHash = hashFor(tailBuffer)

    val hash = "%016x".format(bytesize + headHash + tailHash)
    Hash(hash, bytesize)
  }
}